Reading from the serial port with a timeout (Opl)
=================================================

(An example of Psion-style asynchronous i/o and multiple event sources.)

The following examples were developed on the S3a but would transfer
with only slight changes to any other computer in the Sibo range. 
(Eg the IOC keyword doesn't exist on the S3 and would have to be
replaced with IOA.)

Simple "synchronous" reading from the serial port
-------------------------------------------------

The following program is a cut-down version of that in Appendix C of
the Opl Programming Manual.  It assumes the default baud rate and
handshaking.  Eg go into Windows Terminal on the PC, and set the baud
rate to 9600, and you'll be able to type characters that the program
will receive.

As you'll see, I've skipped all the bits about terminating mask, from
the Programming Manual, that you'll have to put back in for any
serious program of yours.  I've missed these bits out (also the call
to rsset:) so as to concentrate on the feature of adding the timeout.

Here's the first version of the program:

    PROC ttyread:
      local buf$(20),l%,a%,r%
      a%=addr(buf$)
      lopen("tty:a")
      while 1
        l%=20
        r%=iow(-1,1,#(a%+1),l%)
        if r% :raise r% :endif
        pokeb a%,l%
        print buf$
      endwh
    ENDP

Note that you'll have to turn Remote Link off before this program is
able to get past the LOPEN line.  Also be sure to plug in the 3-link
cable.

The problem with this program, of course, is that it hangs forever,
if there are no characters received.  A timeout needs to be added in. 
There are two ways this could be done:
  1/ Via an enhancement to the Serial device driver ("TTY")
  2/ By running a timer as well as the Serial port device driver.

Approach 1/ would be possible (by means of using an attached device
driver), but approach 2/ is *much* more powerful since it can easily
be generalised to cope with yet more complications, such as the need
to respond to keypresses from the user at the same time.

The program with the timeout added:
-----------------------------------

    PROC ttyread:
      local buf$(20),l%,a%
      local th%,tstat%,t&
      local sstat%
      a%=addr(buf$)
      lopen("tty:a")
      l%=ioopen(th%,"TIM:",-1)
      if l% :raise l% :endif
      while 1
        l%=20
        ioc(-1,1,sstat%,#(a%+1),l%)     rem P_FREAD
        t&=100
        ioc(th%,1,tstat%,t&)            rem P_FRELATIVE
        iowait
        if sstat%<>-46
          if sstat%<>0 :raise sstat% :endif
          iow(th%,4,#0,#0)              rem P_FCANCEL
          iowaitstat tstat%
        elseif tstat%<>-46
          iow(-1,4,#0,#0)               rem P_FCANCEL
          iowaitstat sstat%
          iow(-1,10,l%,#0)              rem P_FTEST
          if l%
            iow(-1,1,#(a%+1),l%)        rem P_FREAD
          endif
          print "Serial read timed out"
        else
          raise 1
        endif
        pokeb a%,l%
        print buf$
      endwh
    ENDP

Notes: LOPEN and IOOPEN compared
--------------------------------

The code
    LOPEN("xxx")
    ...
    IOW(-1,...)

is essentially the same as

    r%=IOOPEN(sh%,"xxx",-1)
    if r% :raise r%: endif
    ...
    IOW(sh%,...)

Bearing this in mind, the calls
    LOPEN("TTY:A")
and
    IOOPEN(th%,"TIM:",-1)

can be seen as partners: one opens a channel to the serial device
driver, and the other a channel to the timer device driver.

As you'll see, Opl runtime "hides" the explicit channel handle, in
the case of LOPEN, and supplies it automatically:
    when an i/o handle of -1 is given by the program, or
    when LPRINT is used, etc.

Notes: IOW and IOC compared
---------------------------

The code
    r%=iow(-1,1,#(a%+1),l%)
    if r% :raise r% :endif

in the first version of the program could have been converted to
    ioc(-1,1,sstat%,#(a%+1),l%)     rem P_FREAD
    iowait
    if sstat%<>0 :raise sstat% :endif

and would have worked exactly the same *in that program*.

Internally, an IOW is just an IOC followed by waiting for a signal
and for the given status word to be "completed".

More precisely, the following would be equivalent to the above IOW in
*all* programs:

    ioc(-1,1,sstat%,#(a%+1),l%)     rem P_FREAD
    iowaitstat sstat%
    if sstat%<>0 :raise sstat% :endif

The difference between IOWAITSTAT and IOWAIT:
---------------------------------------------

Your program waits ("hangs") inside a call to IOWAITSTAT SSTAT% until
the i/o request associated with SSTAT% completes.

Your program waits inside a call to IOWAIT until *any* outstanding
i/o request in the application completes.

So, if there are two outstanding requests at one time, and you don't
know which will complete first, you must use IOWAIT instead of
IOWAITSTAT.  (And since IOW implicitly uses IOWAITSTAT, this means
you must use IOC - or IOA - instead of IOW, in all such cases.)

Dealing with two event sources at the same time:
------------------------------------------------

In the critical part of this program, there are *two* outstanding
requests at the same time, made by the following calls:

    l%=20
    ioc(-1,1,sstat%,#(a%+1),l%)     rem P_FREAD
    t&=100
    ioc(th%,1,tstat%,t&)            rem P_FRELATIVE

The first "queues" a read from channel -1 (ie the serial port) for 20
characters.  The second "queues" a timeout from channel th% (ie a
timer) for 100 tenths of seconds (the time being passed as a long in
tenths of seconds).

Once both requests have been made, the program calls IOWAIT and gets
put to bed until one or other event completes.

The other side of IOWAIT
------------------------

After your program comes out of IOWAIT, you have to find which of the
two events have completed.  You do this by looking at the values of
the status words of each of the requests.

In general, the status word records the "completion status" of an i/o
request, which can be one of
    -46 ("E_FILE_PENDING"): the event is still pending, or
    another negative number: the event completed in error, or
    a non-negative number: success of some kind or another.

In these examples, "success" just means a status word with the value
zero, though other i/o channels have multiple "success" states.

In the above program, the two main branches after the IOWAIT give the
cases when, respectively, the serial read has completed, and the
timer has completed.

The stray signal catcher:
-------------------------

As you can see, I have included a *third* branch, with a RAISE 1, to
catch programming errors when an event completes but the program
cannot find out what it is.  At Psion, this is known as a stray
signal (and ROM code panics 143 when any OO program gets a stray
signal).

Stray signals are often caused by errors in *cancelling* outstanding
requests - the subject of the following section.  Rearrange the above
code for your own purposes and you may find yourself being bitten by
the stray signal catcher.

Cancelling outstanding event requests:
--------------------------------------

In the above program, after one serial read has successfully
completed, another one is made, and accordingly, the timeout has to
be reset.  This is done by cancelling the previous request, and then
(at the top of the loop again) requeuing the new timer.

Most device drivers (eg TIM:) take unkindly to having a second i/o
request made on them, while the previous one remains outstanding. 
This is regarded as evidence of a programming error, and you will be
panicked with panic 73.

You cancel an outstanding request using generic i/o service number 4,
P_FCANCEL.  Then you have to IOWAITSTAT to "use up the signal".

Note that, in Epoc, cancels always *precipitate* the completion of
the original event, rather than *withdrawing* it.  There are good
reasons for this - namely, you don't know whether the event has
completed anyway, already.  In either case, the completion generates
a signal, which you have to use up.

Whenever an i/o event occurs
----------------------------

For clarity, let me emphasise that, whenever an i/o event occurs, two
things happen:
  1/ the application gets "signalled"
  2/ the associated status word gets written to.

(In the case of the event occurring via being precipitated with a
cancel, the value -48 (E_FILE_CANCEL) is actually written to the
status word, but this can be disregarded in most cases.)

The meaning of being "signalled" is as follows: the operating system
maintains a special so-called i/o semaphore on behalf of each
application.  (You can see this displayed as one of the columns in SPY.)
Whenever an application calls IOWAIT (or IOWAITSTAT), this value is
decremented by one, and if it is negative, the application hangs
until it becomes non-negative again - on account of some i/o event
completing.  (Whenever an application is signalled, the i/o semaphore
value is incremented.)

Cancelling a serial read request
--------------------------------

The one remaining complication in the above program concerns the
details of cancelling a serial read.  Bear in mind that *some*
characters may have been received already, even though the total
requested amount may not have been.  If this is of interest to your
program, follow the P_FTEST and P_FREAD mechanism in the above
program.  Consult the serial device driver documentation in the C SDK
for some more details.

These notes prepared
--------------------
By David Wood, Software Development, Psion, 9th November 1993.
